Skip to content

feat(staging): render list bullets and title heading depth in markdown element_to_md() function#4384

Open
aaronsteers wants to merge 1 commit into
Unstructured-IO:mainfrom
aaronsteers:copilot/feat-staging-add-listitem-bullets
Open

feat(staging): render list bullets and title heading depth in markdown element_to_md() function#4384
aaronsteers wants to merge 1 commit into
Unstructured-IO:mainfrom
aaronsteers:copilot/feat-staging-add-listitem-bullets

Conversation

@aaronsteers

@aaronsteers aaronsteers commented Jul 1, 2026

Copy link
Copy Markdown

Hello! 👋

I wrote a custom json/dict-to-markdown mapper back in 2024 and found recently you have a built-in mapper now! This PR attempts to fill the parity/feature gap between your built-in native markdown mapper and my custom mapper, so that hopefully I can stop maintaining the custom one. 😀

Specifically, this adds:

  1. L2 and L3 header support (current implementation always generates top-level headers (# Header instead of ## Header or ### Header)
  2. List item handling (- item one instead of item one)

Review in cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="unstructured/staging/base.py">

<violation number="1" location="unstructured/staging/base.py:283">
P1: The new Markdown heading logic is off-by-one because `metadata.category_depth` is zero-indexed in this codebase, but the code treats it as the literal Markdown heading level. For example, an h2 (category_depth=1) renders as `#` instead of `##`, and an h3 (category_depth=2) renders as `##` instead of `###`. This undermines the PR's goal of adding L2/L3 header support.

Also, the converted depth is not clamped to Markdown's valid 1-6 ATX heading range, so edge-case values like `0`, negatives, or values greater than 5 can produce invalid or oversized heading markers. Consider computing the Markdown depth as `max(1, min(category_depth + 1, 6))` and treating `None` as the top-level default (0).</violation>

<violation number="2" location="unstructured/staging/base.py:290">
P2: The new `ListItem` branch renders a Markdown list item by simply prepending `- ` to the text. If `ListItem.text` contains an embedded newline, the generated Markdown becomes `- first line\nsecond line`, where the continuation line starts at column 0 and is parsed as a separate paragraph rather than part of the list item. Since `ListItem` is a plain text element and the serializer does not normalize incoming text, a multiline value from a partitioner or deserialization will corrupt the Markdown output. Consider collapsing embedded newlines to spaces or indenting continuation lines so they stay inside the list item.</violation>
</file>

Shadow auto-approve: would not auto-approve because issues were found.

Re-trigger cubic

case Title(text=text):
return f"# {text}"
case Title(text=text, metadata=metadata):
depth = metadata.category_depth if metadata.category_depth else 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The new Markdown heading logic is off-by-one because metadata.category_depth is zero-indexed in this codebase, but the code treats it as the literal Markdown heading level. For example, an h2 (category_depth=1) renders as # instead of ##, and an h3 (category_depth=2) renders as ## instead of ###. This undermines the PR's goal of adding L2/L3 header support.

Also, the converted depth is not clamped to Markdown's valid 1-6 ATX heading range, so edge-case values like 0, negatives, or values greater than 5 can produce invalid or oversized heading markers. Consider computing the Markdown depth as max(1, min(category_depth + 1, 6)) and treating None as the top-level default (0).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At unstructured/staging/base.py, line 283:

<comment>The new Markdown heading logic is off-by-one because `metadata.category_depth` is zero-indexed in this codebase, but the code treats it as the literal Markdown heading level. For example, an h2 (category_depth=1) renders as `#` instead of `##`, and an h3 (category_depth=2) renders as `##` instead of `###`. This undermines the PR's goal of adding L2/L3 header support.

Also, the converted depth is not clamped to Markdown's valid 1-6 ATX heading range, so edge-case values like `0`, negatives, or values greater than 5 can produce invalid or oversized heading markers. Consider computing the Markdown depth as `max(1, min(category_depth + 1, 6))` and treating `None` as the top-level default (0).</comment>

<file context>
@@ -278,8 +279,16 @@ def element_to_md(
-        case Title(text=text):
-            return f"# {text}"
+        case Title(text=text, metadata=metadata):
+            depth = metadata.category_depth if metadata.category_depth else 1
+            if not isinstance(depth, int):
+                try:
</file context>

except (TypeError, ValueError):
depth = 1
return f"{'#' * depth} {text}"
case ListItem(text=text):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new ListItem branch renders a Markdown list item by simply prepending - to the text. If ListItem.text contains an embedded newline, the generated Markdown becomes - first line\nsecond line, where the continuation line starts at column 0 and is parsed as a separate paragraph rather than part of the list item. Since ListItem is a plain text element and the serializer does not normalize incoming text, a multiline value from a partitioner or deserialization will corrupt the Markdown output. Consider collapsing embedded newlines to spaces or indenting continuation lines so they stay inside the list item.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At unstructured/staging/base.py, line 290:

<comment>The new `ListItem` branch renders a Markdown list item by simply prepending `- ` to the text. If `ListItem.text` contains an embedded newline, the generated Markdown becomes `- first line\nsecond line`, where the continuation line starts at column 0 and is parsed as a separate paragraph rather than part of the list item. Since `ListItem` is a plain text element and the serializer does not normalize incoming text, a multiline value from a partitioner or deserialization will corrupt the Markdown output. Consider collapsing embedded newlines to spaces or indenting continuation lines so they stay inside the list item.</comment>

<file context>
@@ -278,8 +279,16 @@ def element_to_md(
+                except (TypeError, ValueError):
+                    depth = 1
+            return f"{'#' * depth} {text}"
+        case ListItem(text=text):
+            return f"- {text}"
         case Formula(text=text):
</file context>

@aaronsteers aaronsteers changed the title feat(staging): render list bullets and title heading depth in markdown feat(staging): render list bullets and title heading depth in markdown function element_to_md Jul 2, 2026
@aaronsteers aaronsteers changed the title feat(staging): render list bullets and title heading depth in markdown function element_to_md feat(staging): render list bullets and title heading depth in markdown element_to_md() function Jul 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants